﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace IronLua.Metadata
{
    internal static class StreamExtensions
    {

        public static byte[] ReadBytes(this Stream stream, int count)
        {
            var data = new byte[count];

            var bytesRead = stream.Read(data, 0, count);

#if DEBUG
            if (bytesRead != count)
                throw new ArgumentException("Too many bytes to read ({0} of {1} read).".FormatWith(bytesRead, count), "count");
#endif

            return data;
        }

        public static byte Read1Byte(this Stream stream)
        {
            return (byte)stream.ReadByte();
        }

        public static int Read1ByteAsInt32(this Stream stream)
        {
            return (int)stream.Read1Byte();
        }

        public static uint Read1ByteAsUInt32(this Stream stream)
        {
            return (uint)stream.Read1Byte();
        }

        public static byte[] Read4Bytes(this Stream stream)
        {
            return stream.ReadBytes(4);
        }

        public static int ReadLuaSize(this Stream stream)
        {
            return BitConverter.ToInt32(stream.Read4Bytes(), 0);
        }

        public static int ReadLuaInteger(this Stream stream)
        {
            return stream.ReadLuaSize();
        }

        public static string ReadLuaString(this Stream stream)
        {
            return stream.ReadLuaString(stream.ReadLuaSize());
        }

        public static string ReadLuaString(this Stream stream, int size)
        {
            if (size < 0)
                throw new ArgumentOutOfRangeException("size");

            if (size == 0)
                return string.Empty;

            var data = stream.ReadBytes(size);
            var ret = Encoding.ASCII.GetString(data);
            return ret.Substring(0, ret.Length - 1);
        }

        public static double ReadLuaNumber(this Stream stream)
        {
            return BitConverter.ToDouble(stream.ReadBytes(8), 0);
        }

        public static bool ReadLuaBoolean(this Stream stream)
        {
            var bit = stream.Read1Byte();

            if (bit == 0)
                return true;
            else if (bit == 1)
                return false;

            throw new LuaMetadataException("Invalid boolean constant found.");
        }

        public static Chunk ReadLuaChunk(this Stream stream)
        {
            var signature = stream.Read4Bytes().VerifyLuaSignature().ToAscii();
            var ver = stream.Read1Byte().VerifyLuaVersion();
            var format = stream.Read1Byte();
            var endianness = stream.Read1Byte().ToEndianness();
            var intSize = stream.Read1ByteAsUInt32();
            var ptrSize = stream.Read1ByteAsUInt32();
            var instrSize = stream.Read1ByteAsUInt32();
            var numSize = stream.Read1ByteAsUInt32();
            var numType = stream.Read1Byte().ToNumberType();
            var tail = new byte[] { 
                stream.Read1Byte(), 
                stream.Read1Byte(), 
                stream.Read1Byte(), 
                stream.Read1Byte(), 
                stream.Read1Byte(), 
                stream.Read1Byte() 
            }.VerifyChunkHeaderTail();

            var top = stream.ReadPrototype();
            var protos = top.Prototypes;

            return new Chunk()
            {
                Name = top.Name,
                Signature = signature,
                Version = ver,
                Format = format,
                Endianness = endianness,
                IntegerSize = intSize,
                PtrSize = ptrSize,
                InstructionSize = instrSize,
                NumberSize = numSize,
                NumberType = numType,
                TopFunction = top,
                Prototypes = protos,                
            };
        }

        public static Prototype ReadPrototype(this Stream stream, uint index = 0, uint depth = 0)
        {
            var lineDef = (uint)stream.ReadLuaInteger();
            var lastLineDef = (uint)stream.ReadLuaInteger();
            var parmCount = stream.Read1ByteAsUInt32();
            var vaFlags = stream.Read1Byte().ToVarArgFlags();
            var maxStack = (uint)stream.Read1ByteAsInt32();

            var code = stream.ReadInstructions();
            var consts = stream.ReadConstants();
            var protos = stream.ReadPrototypes(depth + 1);
            var ups = stream.ReadUpValues();

            var name = stream.ReadLuaString();
            var lineInfo = stream.ReadSourceLines();
            var locals = stream.ReadLocals();

            ups = stream.ReadUpValueNames(ups);

            return new Prototype()
            {
                Kind = PrototypeKind.Bytecode,
                Name = name,
                Depth = depth,
                Index = index,
                LineDefined = lineDef,
                LastLineDefined = lastLineDef,
                ParameterCount = parmCount,
                Instructions = code,
                UpValueMetadata = ups,
                VarArgFlags = vaFlags,
                StackSize = maxStack,
                Constants = consts,
                Prototypes = protos,
                LocalsMetadata = locals,
                SourceLines = lineInfo,                              
            };
        }

        internal static IList<Instruction> ReadInstructions(this Stream stream)
        {
            var icount = stream.ReadLuaInteger();
            var instrs = new List<Instruction>(icount);

            for (int i = 0; i < icount; i++)
            {
                instrs.Add(stream.ReadInstruction());
            }

            return instrs;
        }

        internal static IList<Constant> ReadConstants(this Stream stream)
        {
            var kcount = stream.ReadLuaInteger();
            var consts = new List<Constant>(kcount);

            for (int i = 0; i < kcount; i++)
            {
                consts.Add(stream.ReadConstant(i));
            }

            return consts;
        }

        internal static IList<Prototype> ReadPrototypes(this Stream stream, uint depth)
        {
            var count = stream.ReadLuaInteger();
            var protos = new Prototype[count];
            for (uint i = 0; i < count; i++)
            {
                protos[i] = stream.ReadPrototype(i, depth);
            }
            return protos;
        }

        internal static IList<int> ReadSourceLines(this Stream stream)
        {
            var slcount = stream.ReadLuaInteger();
            var lines = new List<int>(slcount);

            for (int i = 0; i < slcount; i++)
            {
                lines.Add(stream.ReadSourceLine(i));
            }

            return lines;
        }

        internal static IList<Local> ReadLocals(this Stream stream)
        {
            var lcount = stream.ReadLuaInteger();
            var locals = new List<Local>(lcount);

            for (int i = 0; i < lcount; i++)
            {
                locals.Add(stream.ReadLocal(i));
            }

            return locals;
        }

        internal static IList<UpValue> ReadUpValues(this Stream stream)
        {
            var count = stream.ReadLuaInteger();
            var ups = new UpValue[count];
            for (int i = 0; i < count; i++)
            {
                ups[i] = new UpValue(stream.ReadLuaBoolean(), stream.Read1ByteAsUInt32());
            }

            return ups;
        }

        internal static Instruction ReadInstruction(this Stream stream)
        {
            return new Instruction((uint)stream.ReadLuaInteger());
        }

        internal static Constant ReadConstant(this Stream stream, int index)
        {
            var constType = stream.Read1Byte().ToConstantType();

            switch (constType)
            {
                case ConstantTypes.Boolean:
                    return stream.ReadLuaBooleanConstant(index);
                case ConstantTypes.Number:
                    return stream.ReadLuaNumberConstant(index);
                case ConstantTypes.String:
                    return stream.ReadLuaStringConstant(index);
            }

            return stream.ReadLuaNilConstant(index);
        }

        internal static int ReadSourceLine(this Stream stream, int index)
        {
            return stream.ReadLuaInteger();
        }

        internal static Local ReadLocal(this Stream stream, int index)
        {
            return new Local(index, stream.ReadLuaString(), stream.ReadLuaInteger(), stream.ReadLuaInteger());
        }

        internal static IList<UpValue> ReadUpValueNames(this Stream stream, IList<UpValue> ups)
        {
            var count = stream.ReadLuaInteger();
            IList<UpValue> ups2 = new UpValue[ups.Count];

            for (int i = 0; i < count; i++)
            {
                ups2[i] = new UpValue(ups[i].InStack, ups[i].Index, stream.ReadLuaString());
            }

            return ups2;
        }

        internal static Constant ReadLuaNilConstant(this Stream stream, int index)
        {
            return new Constant(index);
        }

        internal static Constant ReadLuaBooleanConstant(this Stream stream, int index)
        {
            return new Constant(index, stream.ReadLuaBoolean());
        }

        internal static Constant ReadLuaNumberConstant(this Stream stream, int index)
        {
            return new Constant(index, stream.ReadLuaNumber());
        }

        internal static Constant ReadLuaStringConstant(this Stream stream, int index)
        {
            return new Constant(index, stream.ReadLuaString());
        }

    }
}
